from google.appengine.api import memcache
from google.appengine.ext import db
import random
import logging
import time
import datetime
class GeneralCounterShardConfig(db.Model):
  """Tracks the number of shards for each named counter."""
  name = db.StringProperty(required=True)
  num_shards = db.IntegerProperty(required=True, default=20)
  
class GeneralCounterShard(db.Model):
  """Shards for each named counter"""
  name = db.StringProperty(required=True)
  count = db.IntegerProperty(required=True, default=0)
class Locks(db.Model):
	"""
		Lock on a single data item
	"""
	din= db.StringProperty(required=True)
	mode=db.StringProperty(choices=set(["r","w"]))
	transactions = db.ListProperty(db.Key)
	@staticmethod
	def removetransaction(trans):
		dataitems = []
		for lock in Locks.all():
			transactions = Transactions.get(lock.transactions)
			for t in transactions:
				if t.tid == trans.tid:
					logging.info("Releasing lock on %s held by %d" %(lock.din,t.tid))
					dataitems.append(lock.din)
					lock.transactions.remove(t.key())
					if len(lock.transactions) == 0:
						lock.mode = None
					lock.put()
					break
		return dataitems
		
	@staticmethod	
	def getlockrecord(din):
		l=Locks.gql("WHERE din = :1",din).get()
		if l is None:
			l=Locks(din=din)
		return l


class OCCTrans (db.Model):
	sno = db.IntegerProperty(required=True)
	tid = db.IntegerProperty()
	rs = db.ListProperty(db.Key)
	ws = db.ListProperty(db.Key)
	activeset = db.ListProperty(db.Key)
	fstart = db.IntegerProperty()
	fend=db.IntegerProperty()
	phase = db.StringProperty(choices=set(["read","val","write","comitted","aborted"]), default="read")
	starttime=db.DateTimeProperty(auto_now_add=True)
	endtime=db.DateTimeProperty()
	isuseraborted = db.BooleanProperty(default=False)
	@staticmethod
	def begin(sno):
		fstart = int(get_count("occ_trans"))
		logging.info("beginning a new transaction fstart = %d" % fstart)
		o = OCCTrans(sno=sno,fstart=fstart)
		o.put()
		return o
	def addop(self,o):
		if not o.is_saved():
			o.put()
		returnVal = None
		if o.optype == "r":
			logging.info("read")
			self.rs.append(o.key())
			#must read a value here. first try the local write set if that has written this value else fetch from DataItems
			#find the occ operation which is write and of this transaction else fetch from data store
			ttwrite = OCCOperations.gql("WHERE trans =:1 and optype=:2 and din = :3",self,"w",o.din).get() 

			if ttwrite:
				logging.info("found a write in this transaction only")
				returnVal = ttwrite.div
			else:
				logging.info("Need to get from DB")
				di = DataItems.gql("WHERE din = :1",o.din).get()
				if di:
					returnVal = di.div
				else:
					returnVal =  "0"
			o.div = returnVal
			o.put()

		elif o.optype == "w":
			self.ws.append(o.key())
			returnVal = o.div
		elif o.optype == "c" or o.optype == "a":
			returnVal = True
		self.put()
		return returnVal
	#this needs to be done using a critical section
	def finishread(self):
		c=get_count("occ_trans")
		logging.info("counter = %s" % c)
		self.fend = int(c)
		for trans in OCCTrans.gql("WHERE phase = :1","val"):
			self.activeset.append(trans.key())
		self.phase = "val"
		self.put()
	def finishreadonly(self):
		c=get_count("occ_trans")
		logging.info("counter = %s" % c)
		self.fend = int(c)
		self.phase = "comitted"
		self.put()
	def invalidate(self):
		self.phase = "aborted"
		self.fstart = None
		self.fend = None
		self.activeset = []
		self.endtime=datetime.datetime.now()
		self.put()
		
	def commit(self):
		self.phase="write"
		increment("occ_trans")
		self.tid = int(get_count("occ_trans"))
		self.put()
		
		for op in OCCOperations.get(self.rs):
			OCCLog.addop(op)
			
		for op in OCCOperations.get(self.ws):
			OCCLog.addop(op)
			di = DataItems.gql("WHERE din = :1", op.din).get()
			if not di:
				di = DataItems(din=op.din,div=op.div)
			else:
				di.div = op.div
			di.put()
		
		self.phase = "comitted"
		self.endtime=datetime.datetime.now()
		self.put()
			
			

def isIntersecting(set1,set2):
	for op1 in OCCOperations.get(set1):
		for op2 in OCCOperations.get(set2):
			if op1.din == op2.din:
				return True
	return False

class Mutex(db.Model):
	#represents if m1 is locked
	lock=db.BooleanProperty(default=False)
	sid=db.StringProperty()
	def lockup(self):
		if not self.lock:
			self.lock = True
			self.put()
			return True
		else:
			return False
	def unlock(self):
		self.lock = False
		self.put()
		
	
	
class OCCOperations(db.Model):
	din = db.StringProperty()
	div = db.StringProperty()
	optype = db.StringProperty(choices=set(["r","w","c","a"]))
	trans = db.ReferenceProperty(OCCTrans,required=True)
	ca=db.DateTimeProperty(auto_now_add=True)
	
class OCCLog(OCCOperations):
	la=db.DateTimeProperty(auto_now_add=True)
	@staticmethod
	def addop(op):	
		OCCLog(din=op.din,div=op.div,optype=op.optype,trans=op.trans,ca=op.ca).put()
		
class Transactions(db.Model):
	"""
		Represents a single transaction
	"""
	tid = db.IntegerProperty(required=True)
	ops = db.ListProperty(db.Key)
	starttime = db.DateTimeProperty(auto_now_add=True)
	endtime = db.DateTimeProperty()
	isaborted = db.BooleanProperty(default=False)
	#here excluding op is the one thats actually doing the query
	def haswaitingops(self,maxcount=1):
		return (Operations.gql("WHERE transaction = :1 and iswaiting=:2",self,True).count(limit=maxcount)) == maxcount
			

def createbytid(tid):
	t = Transactions.gql("WHERE tid = :1",tid).get()
	if t is not None:
		return t
	else: 
		t=Transactions(tid=tid,ops=[])
		t.put()
		return t
		
class Operations(db.Model):	
	"""
		Represents a single operation. div - data item value will be None in case of a read, but when its entered into the log it will have a value.
	"""
	transaction = db.ReferenceProperty(Transactions,required=True)
	#din will not be present for commit or abort
	din= db.StringProperty()
	#the value before this transaction is written
	beforeval = db.StringProperty()
	div = db.StringProperty()
	ets = db.DateTimeProperty(auto_now_add=True)
	iswaiting = db.BooleanProperty(default=False)
	optype = db.StringProperty(choices=set(["r","w","c","a"]))
	ca = db.DateTimeProperty(auto_now_add=True)
	waitstarttime=db.DateTimeProperty()
	waitendtime=db.DateTimeProperty()
	isinverseop = db.BooleanProperty(default=False)
	def perform(self):
		self.waitendtime = datetime.datetime.now()
		self.iswaiting = False
		
		if self.optype == "r" or self.optype == "w":
			logging.info(("Performing op: %s on %s with value %s" % (self.optype,self.din,self.div)))
			dataitem = DataItems.gql("WHERE din=:1",self.din).get()
			if dataitem is None:
				dataitem = DataItems(din=self.din,div="0")
				dataitem.put()
			if self.optype == "r":
				self.div = dataitem.div
				self.put()
				Log.add(self)
				return dataitem.div
			elif self.optype == "w":
				self.beforeval = dataitem.div
				dataitem.div = self.div
				self.put()
				dataitem.put()
				Log.add(self)
				return dataitem.div
		elif self.optype == "c" or self.optype == "a":
			logging.info("Performing commit")
			self.transaction.endtime=datetime.datetime.now()	
			self.put()
			self.transaction.put()
			
			return "Done"
				
		
		
			
class DataItems(db.Model):
	din=db.StringProperty(required=True)
	div=db.StringProperty(required=True)
	
	
class Log(Operations):
	la=db.DateTimeProperty(auto_now_add=True)
	"""
		an entry into this is made when an operation is executed
	"""
	@staticmethod
	def add(op):
		l=Log(transaction=op.transaction,din=op.din,div=op.div,ets=op.ets,iswaiting=op.iswaiting,optype=op.optype,beforeval=op.beforeval,isinverseop=op.isinverseop)
		l.put()
	
#this should put in the before value in case of a write op
def save(op,lock=None):
	if(lock):
		lock.put()
	if(op):
		op.put()
		if op.key() not in op.transaction.ops:
			op.transaction.ops.append(op.key())
			op.transaction.put()

def logop(waitingop):
	waitingop.put()
	Log.add(op)
	
def getallwaitingops(transaction,ignoreme=None):
	if not ignoreme:
		return Operations.gql("WHERE iswaiting = :1",True)
	else:
		return Operations.gql("WHERE iswaiting = :1 and key != :2",True,ignoreme.key())
def get_count(name):
  """Retrieve the value for a given sharded counter.
 
  Parameters:
    name - The name of the counter 
  """
  total = memcache.get(name)
  if total is None:
    total = 0
    for counter in GeneralCounterShard.all().filter('name = ', name):
      total += counter.count
    memcache.add(name, str(total), 60)
  return total

 
def increment(name):
  """Increment the value for a given sharded counter.
 
  Parameters:
    name - The name of the counter 
  """
  config = GeneralCounterShardConfig.get_or_insert(name, name=name)
  def txn():
    index = random.randint(0, config.num_shards - 1)
    shard_name = name + str(index)
    counter = GeneralCounterShard.get_by_key_name(shard_name)
    if counter is None:
      counter = GeneralCounterShard(key_name=shard_name, name=name)
    counter.count += 1
    counter.put()
  db.run_in_transaction(txn)
  memcache.incr(name)

 
def increase_shards(name, num): 
  """Increase the number of shards for a given sharded counter.
  Will never decrease the number of shards.
 
  Parameters:
    name - The name of the counter
    num - How many shards to use
   
  """
  config = GeneralCounterShardConfig.get_or_insert(name, name=name)
  def txn():
    if config.num_shards < num:
      config.num_shards = num
      config.put()   
  db.run_in_transaction(txn)
  
  
def getlocktable():
	locktable  = MasterLockTable.all().fetch(1)[0]
	return locktable
	
def savelocktable(locktable):
	locktable.put()
	
def getlocktableforadmin():
	return MasterLockTable.all().fetch(1)[0]
def unlocklocktable():
	ml = MasterLockTable.all().fetch(1)[0]
	ml.di=[]
	ml.islocked=False
	ml.put()


